package com.yixing.tech.wms.service.impl;

import com.yixing.tech.common.base.BusinessException;
import com.yixing.tech.common.base.CustomAggregationOperation;
import com.yixing.tech.common.convert.ConvertUtils;
import com.yixing.tech.common.service.AbstractWmsService;
import com.yixing.tech.wms.entity.InStockWork;
import com.yixing.tech.wms.service.StockService;
import com.yixing.tech.wms.utils.DocumentUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

import static com.yixing.tech.common.CommonConstant.*;
import static com.yixing.tech.common.CommonConstant.ContainerType.*;
import static com.yixing.tech.common.CommonConstant.InStatus.inStockOK;
import static com.yixing.tech.common.CommonConstant.InStatus.inStocking;

/**
 * StockService
 *
 * @author yixing tech
 * @since 1.0.0
 */
@Slf4j
@Service
public class StockServiceImpl extends AbstractWmsService implements StockService {

    @Value("${tenant-id: 6204cb763da841718ddd1cf8}")
    private String tenantId;

    private Double safeGetDouble(Document doc, String field) {
        return ConvertUtils.convertIfPossible(doc.get(field), Double.class, 0D);
    }

    /**
     * 根据上架作业更新库存信息
     *
     * @param upTaskCode 上架作业
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateInStock(String upTaskCode) throws BusinessException {
        if (StringUtils.isBlank(upTaskCode)) {
            throw new BusinessException("上架任务不能为空");
        }

        // 校验需要存在入库作业
        InStockWork inStockTask = mongoTemplate.findOne(Query.query(newCriteria().and("inStatus.id").is(new ObjectId(inStocking.getCode()))
                        .and("code").is(upTaskCode)),
                InStockWork.class, "entity_" + IN_STOCK_TASK);
        if (inStockTask == null) {
            throw new BusinessException("上架作业不存在");
        }

        boolean success;
        String inStockTypeCode = Optional.ofNullable(inStockTask.getInStockType()).map(x -> x.getString("id")).orElse("");
        if (InStockType.rawMaterialInStock.getCode().equals(inStockTypeCode) || InStockType.otherInStock.getCode().equals(inStockTypeCode)) {
            success = updateInStockV3(inStockTask, RAW_MATERIAL_IN_STOCK_DETAIL, upTaskCode);
        } else if (InStockType.productionInStock.getCode().equals(inStockTypeCode)) {
            success = updateInStockV3(inStockTask, PRODUCTION_IN_STOCK_DETAIL, upTaskCode);
        } else if (InStockType.salesReturnStock.getCode().equals(inStockTypeCode)) {
            success = updateInStockV3(inStockTask, SALES_RETURN_STOCK_DTL, upTaskCode);
        } else {
            throw new BusinessException("不支持的上架作业类型");
        }
        return success;
    }


    private boolean insertFlowV2(InStockWork inStockTask, Collection<Document> stockList) {
        List<Document> flowList = new ArrayList<>();
        for (Document stock : stockList) {
            // 出入库流水
            Document flow = new Document();
            flow.put("createTime", new Date());
            flow.put("tenantId", tenantId);
            flow.put("isDelete", 0);

            flow.put("order", inStockTask.getInOrder());
            flow.put("taskCode", inStockTask.getCode());
            flow.put("taskObjName", "上架作业");
            flow.put("flowBizType", newDocument("id", new ObjectId(FlowBizType.inOut.getCode())));
            flow.put("locationCode", stock.get("locationCode"));
            flow.put("containerCode", stock.get("containerCode"));
            flow.put("materialCode", stock.get("materialCode"));
            flow.put("partitionSide", stock.get("partitionSide"));

            flow.put("batchNo", stock.get("batchNo"));
            flow.put("num", stock.get("realNum"));
            flowList.add(flow);
        }
        mongoTemplate.insert(flowList, "entity_" + STOCK_FLOW);
        return true;
    }

    private boolean insertFlow(Document inStockTask, Collection<Document> stockList) {
        List<Document> flowList = new ArrayList<>();
        for (Document stock : stockList) {
            // 出入库流水
            Document flow = new Document();
            flow.put("createTime", new Date());
            flow.put("tenantId", tenantId);
            flow.put("isDelete", 0);

            flow.put("order", inStockTask.get("inOrder"));
            flow.put("taskCode", inStockTask.get("code"));
            flow.put("taskObjName", "上架作业");
            flow.put("flowBizType", newDocument("id", new ObjectId(FlowBizType.inOut.getCode())));
            flow.put("locationCode", stock.get("locationCode"));
            flow.put("containerCode", stock.get("containerCode"));
            flow.put("materialCode", stock.get("materialCode"));
            flow.put("partitionSide", stock.get("partitionSide"));

            flow.put("batchNo", stock.get("batchNo"));
            flow.put("num", stock.get("realNum"));
            flowList.add(flow);
        }
        mongoTemplate.insert(flowList, "entity_" + STOCK_FLOW);
        return true;
    }

    /**
     * 更新库存，纯累加数据。解锁库位、容器
     */
    private boolean updateInStockV3(InStockWork inStockTask, String dtlTableName, String upTaskCode) throws BusinessException {
        // 查询容器
        String containerCode = inStockTask.getContainerCode();
        Document container = simpleFindOne("entity_" + CONTAINER, "code", containerCode);
        if (container == null) {
            throw new BusinessException("容器不存在");
        }

        // 查询库位
        String locationCode = inStockTask.getLocationCode();
        Document location = simpleFindOne("entity_" + STOCK_LOCATION, "code", locationCode);
        if (location == null) {
            throw new BusinessException("库区不存在");
        }

        // 更新上架作业：入库状态改为入库成功
        Update update = new Update();
        update.set("inStatus", newDocument("id", new ObjectId(inStockOK.getCode())));
        if (!updateById(inStockTask.get_id().toString(), update, "entity_" + IN_STOCK_TASK)) {
            throw new BusinessException("更新上架作业失败！");
        }

        List<Document> stockList = new LinkedList<>();
        // 更新入库单明细实际入库数量
        // 创建库存信息，细化到批次
        double realNum = inStockTask.getRealNum();
        Update update4 = new Update();
        String batchNo = inStockTask.getBatchNo();

        Document dtlData = simpleFindOne("entity_" + dtlTableName, "_id", inStockTask.getInOrderDtlId());
        double oldRealInNum = ConvertUtils.convertIfPossible(dtlData.get("realInNum"), Double.class, 0D);
        update4.set("realInNum", oldRealInNum + realNum);
        updateById(dtlData, update4, "entity_" + dtlTableName);

        String areaCode = inStockTask.getAreaCode();
        String warehouseCode = "";
        Document area = simpleFindOne("entity_" + STOCK_AREA, "code", areaCode);
        if(area != null) {
            warehouseCode = area.getString("warehouse");
        }
        Document stock = new Document();
        stock.put("warehouseCode", warehouseCode);
        stock.put("areaCode", areaCode);
        stock.put("locationCode", inStockTask.getLocationCode());
        stock.put("containerCode", inStockTask.getContainerCode());
        stock.put("materialCode", inStockTask.getMaterialCode());
        stock.put("partitionSide", inStockTask.getPartitionSide());
        stock.put("colorName", inStockTask.getColorName());
        stock.put("languageName", inStockTask.getLanguageName());
        stock.put("containerType", DocumentUtils.wrapDictItem(DocumentUtils.unwrapDictItem(inStockTask.getContainerType())));
        stock.put("batchNo", batchNo);
        stock.put("initNum", realNum);
        stock.put("realNum", realNum);
        // 关联上架作业
        stock.put("upJobCode", upTaskCode);
        // 关联来源单号、来源单行号
        stock.put("inSource", inStockTask.getInSource());
        stock.put("inDetailSource", inStockTask.getInDetailSource());

        stock.put("createTime", new Date());
        stock.put("tenantId", tenantId);
        stock.put("isDelete", 0);
        stockList.add(stock);

        mongoTemplate.insert(stockList, "entity_" + STOCK_NAME);

        // 库存流水
        if (!insertFlowV2(inStockTask, stockList)) {
            throw new BusinessException("库存流水新增失败");
        }

        return true;
    }

    /**
     * 更新库存，纯累加数据。解锁库位、容器
     */
    private boolean updateInStockV2(Document inStockTask, String dtlTableName, String upTaskCode) throws BusinessException {
        List<Document> inStockDetails = inStockTask.getList("inOrders", Document.class);
        if (CollectionUtils.isEmpty(inStockDetails)) {
            throw new BusinessException("入库单明细不存在");
        }

        // 查询容器
        String containerCode = inStockTask.getString("containerCode");
        Document container = simpleFindOne("entity_" + CONTAINER, "code", containerCode);
        if (container == null) {
            throw new BusinessException("容器不存在");
        }

        // 查询库位
        String locationCode = inStockTask.getString("locationCode");
        Document location = simpleFindOne("entity_" + STOCK_LOCATION, "code", locationCode);
        if (location == null) {
            throw new BusinessException("库区不存在");
        }

        // 更新上架作业：入库状态改为入库成功
        Update update = new Update();
        update.set("inStatus", newDocument("id", new ObjectId(inStockOK.getCode())));
        if (!updateById(inStockTask, update, "entity_" + IN_STOCK_TASK)) {
            throw new BusinessException("更新上架作业失败！");
        }

        // 解锁容器
        Update update2 = new Update();
        update2.set("lock", false);
        if (!updateById(container, update2, "entity_" + CONTAINER)) {
            throw new BusinessException("容器解锁失败！");
        }
        // 库位与容器绑定并解锁库位
        Update update3 = new Update();
        update3.set("lock", false);
        update3.set("containerCode", containerCode);
        if (!updateById(location, update3, "entity_" + STOCK_LOCATION)) {
            throw new BusinessException("库位解锁失败！");
        }
        List<Document> stockList = new LinkedList<>();
        // 更新入库单明细实际入库数量
        // 创建库存信息，细化到批次
        double realNum = ConvertUtils.convertIfPossible(inStockTask.get("realNum"), Double.class, 0D);
        for (Document inStockDetail : inStockDetails) {
            Update update4 = new Update();
            String batchNo = inStockDetail.getString("batchNo");
            double dtlRealNum = ConvertUtils.convertIfPossible(inStockDetail.get("matchNum"), Double.class, 0D);
            double updateNum;
            if (realNum > dtlRealNum) {
                updateNum = dtlRealNum;
                realNum -= dtlRealNum;
            } else {
                updateNum = realNum;
                realNum = 0;
            }

            Document dtlData = simpleFindOne("entity_" + dtlTableName, "_id", new ObjectId(inStockDetail.getString("id")));
            double oldRealInNum = ConvertUtils.convertIfPossible(dtlData.get("realInNum"), Double.class, 0D);
            update4.set("realInNum", oldRealInNum + updateNum);
            updateById(dtlData, update4, "entity_" + dtlTableName);

            Document stock = new Document();
            stock.put("warehouseCode", inStockTask.get("warehouseCode"));
            stock.put("areaCode", inStockTask.get("areaCode"));
            stock.put("locationCode", inStockTask.get("locationCode"));
            stock.put("containerCode", inStockTask.get("containerCode"));
            stock.put("materialCode", inStockDetail.get("materialCode"));
            stock.put("partitionSide", inStockDetail.get("partitionSide"));
            stock.put("containerType", DocumentUtils.wrapDictItem(DocumentUtils.unwrapDictItem(inStockTask.get("containerType", Document.class))));
            stock.put("batchNo", batchNo);
            stock.put("initNum", updateNum);
            stock.put("realNum", updateNum);
            // 关联上架作业
            stock.put("upJobCode", upTaskCode);
            // 关联来源单号、来源单行号
            stock.put("inSource", inStockDetail.get("inSource"));
            stock.put("inDetailSource", inStockDetail.get("inDetailSource"));

            stock.put("createTime", new Date());
            stock.put("tenantId", tenantId);
            stock.put("isDelete", 0);
            stockList.add(stock);
        }

        mongoTemplate.insert(stockList, "entity_" + STOCK_NAME);

        // 库存流水
        if (!insertFlow(inStockTask, stockList)) {
            throw new BusinessException("库存流水新增失败");
        }

        return true;
    }


    /**
     * 更新库存，会累加数量
     */
    private boolean updateInStock(Document inStockTask, String dtlObj) throws BusinessException {
        List<Document> inStockDetails = inStockTask.getList("inOrders", Document.class);
        if (CollectionUtils.isEmpty(inStockDetails)) {
            throw new BusinessException("入库单明细不存在");
        }

        // 查询容器
        String containerCode = inStockTask.getString("containerCode");
        Document container = simpleFindOne("entity_" + CONTAINER, "code", containerCode);
        if (container == null) {
            throw new BusinessException("容器不存在");
        }

        // 查询库位
        String locationCode = inStockTask.getString("locationCode");
        Document location = simpleFindOne("entity_" + STOCK_LOCATION, "code", locationCode);
        if (location == null) {
            throw new BusinessException("库区不存在");
        }

        // 更新上架作业：入库状态改为入库成功
        Update update = new Update();
        update.set("inStatus", newDocument("id", new ObjectId(inStockOK.getCode())));
        if (!updateById(inStockTask, update, "entity_" + IN_STOCK_TASK)) {
            throw new BusinessException("更新上架作业失败！");
        }

        // 解锁容器
        Update update2 = new Update();
        update2.set("lock", false);
        if (!versionUpdateById(container, update2, "entity_" + CONTAINER)) {
            throw new BusinessException("容器解锁失败！");
        }
        // 解锁库位
        Update update3 = new Update();
        update3.set("lock", false);
        if (!versionUpdateById(location, update3, "entity_" + STOCK_LOCATION)) {
            throw new BusinessException("库位解锁失败！");
        }

        Map<String, Document> stockMap = new LinkedHashMap<>();

        // 更新入库单明细实际入库数量
        // 创建库存信息，细化到批次
        double realNum = ConvertUtils.convertIfPossible(inStockTask.get("realNum"), Double.class, 0D);
        for (Document inStockDetail : inStockDetails) {
            Update update4 = new Update();
            String batchNo = inStockDetail.getString("batchNo");
            String materialCode = inStockDetail.getString("materialCode");
            String partitionSide = inStockDetail.getString("partitionSide");
            String key = materialCode + ":" + batchNo + ":" + partitionSide;
            double dtlRealNum = ConvertUtils.convertIfPossible(inStockDetail.get("matchNum"), Double.class, 0D);
            double updateNum;
            if (realNum > dtlRealNum) {
                updateNum = dtlRealNum;
                realNum -= dtlRealNum;
            } else {
                updateNum = realNum;
                realNum = 0;
            }

            Document dtlData = simpleFindOne("entity_" + dtlObj, "_id", new ObjectId(inStockDetail.getString("id")));
            double oldRealInNum = ConvertUtils.convertIfPossible(dtlData.get("realInNum"), Double.class, 0D);
            update4.set("realInNum", oldRealInNum + updateNum);
            updateById(dtlData, update4, "entity_" + dtlObj);

            Document stock = stockMap.get(key);
            if (stock == null) {
                stock = new Document();
                stock.put("warehouseCode", inStockTask.get("warehouseCode"));
                stock.put("areaCode", inStockTask.get("areaCode"));
                stock.put("locationCode", inStockTask.get("locationCode"));
                stock.put("containerCode", inStockTask.get("containerCode"));
                stock.put("materialCode", inStockDetail.get("materialCode"));
                stock.put("partitionSide", inStockDetail.get("partitionSide"));
                stock.put("containerType", DocumentUtils.wrapDictItem(DocumentUtils.unwrapDictItem(inStockTask.get("containerType", Document.class))));
                stock.put("batchNo", batchNo);
                stock.put("initNum", updateNum);
                stock.put("realNum", updateNum);
                stock.put("createTime", new Date());
                stock.put("tenantId", tenantId);
                stock.put("isDelete", 0);
                stockMap.put(key, stock);
            } else {
                double oldNum = safeGetDouble(stock, "realNum"); // stock.getDouble("realNum");
                stock.put("initNum", oldNum + updateNum);
                stock.put("realNum", oldNum + updateNum);
            }
        }

        mongoTemplate.insert(stockMap.values(), "entity_" + STOCK_NAME);

        // 库存流水
        if (!insertFlow(inStockTask, stockMap.values())) {
            throw new BusinessException("库存流水新增失败");
        }

        return true;
    }

    /**
     * 查询符合条件的容器，单个物料匹配容器
     */
    @Override
    public List<Document> queryMatchContainers(String containerType, List<String> areas, String materialCode,
                                               String batchNo, double capacity, Collection<String> excludeContainerCodes) throws BusinessException {
        // 根据容器类型筛选出容器编码
        Criteria criteria = newCriteria().and("type").is(newDocument("id", new ObjectId(containerType)))
                .and("lock").is(false);
        if(!CollectionUtils.isEmpty(excludeContainerCodes)) {
            criteria.and("code").nin(excludeContainerCodes);
        }
        Query query = Query.query(criteria);
        query.fields().include("code");
        List<Document> containers = mongoTemplate.find(query, Document.class, "entity_" + CONTAINER);
        assertBiz(() -> CollectionUtils.isEmpty(containers), "没有类型[%s]空余的容器或都被锁定", containerType);
        List<String> containerCodes = containers.stream().map(x -> x.getString("code")).collect(Collectors.toList());

        // 根据库区进行再次筛选
        Criteria match = newCriteria().and("area").in(areas)
                .and("lock").is(false).and("containerCode").in(containerCodes);
        List<AggregationOperation> bindOperations = new LinkedList<>();
        bindOperations.add(Aggregation.match(match));

        String sql = "{ $lookup:{ " +
                "from:'entity_6690d464634ce45c56c08921'," +
                "let:{ containerCode:'$containerCode', locationCode:'$code'}," +
                "pipeline:[" +
                "{$match:{$and:[{$expr:{$eq:['$containerCode','$$containerCode']}},{$expr:{$eq:['$isDelete',0]}}, " +
                "{$expr:{$gt:['$realNum', 0]}}, {$expr:{$eq:['$locationCode','$$locationCode']}}]}}" +
                "{$project:{materialCode:1, partitionSide:1, batchNo: 1, realNum: 1, _id:0}}" +
                "]," +
                "as:'stocks'}}";
        bindOperations.add(new CustomAggregationOperation(sql));
        bindOperations.add(Aggregation.lookup("entity_" + STOCK_AREA, "areaCode", "code", "areaObj"));
        bindOperations.add(Aggregation.unwind("areaObj", true));
        bindOperations.add(Aggregation.addFields()
                .addFieldWithValue("warehouseCode", "$areaObj.warehouse")
                .addFieldWithValue("areaCode", "$area")
                .addFieldWithValue("locationCode", "$code")
                .build());
        bindOperations.add(Aggregation.project().andInclude("warehouseCode", "areaCode", "locationCode", "containerCode", "stocks"));
        List<Document> locationStocks = aggregateResult(bindOperations, "entity_" + STOCK_LOCATION);

        // 根据类型来指定分隔数量，默认是普通的料箱
        int partitionSize;
        if (partitionBox.getCode().equals(containerType)) {
            partitionSize = 2;
        } else if (partitionBox3.getCode().equals(containerType)) {
            partitionSize = 3;
        } else if (partitionBox4.getCode().equals(containerType)) {
            partitionSize = 4;
        } else if (bigRack4.getCode().equals(containerType)) {
            partitionSize = 4;
        } else if (lightweight6.getCode().equals(containerType)) {
            partitionSize = 6;
        } else if (productBox2.getCode().equals(containerType)) {
            partitionSize = 2;
        } else {
            partitionSize = 1;
        }

        double zero = 0D;

        // 整理数据
        // 隔板容器：根据隔板划分的空间来将容器划分开
        // 算出已使用空间
        // 按照已使用空间从大到校排列
        List<Document> result = new LinkedList<>();
        locationStocks.forEach(x -> {
            List<Document> stocks = x.getList("stocks", Document.class);
            if (stocks.isEmpty()) {
                for (int i = 0; i < partitionSize; i++) {
                    Document doc = new Document(x);
                    doc.put("use", zero);
                    doc.put("priority", zero);
                    doc.put("partitionSide", i + 1 + "");
                    result.add(doc);
                }
            } else {
                Map<String, List<Document>> stockListMap = new LinkedHashMap<>();
                stocks.forEach(s -> stockListMap.computeIfAbsent(s.getString("partitionSide"), a -> new LinkedList<>()).add(s));
                double priority = 0;
                List<Document> list = new LinkedList<>();
                for (int i = 0; i < partitionSize; i++) {
                    String partitionSide = i + 1 + "";
                    List<Document> v = stockListMap.get(partitionSide);

                    if (v != null) {
                        String stockMaterialCode = v.get(0).getString("materialCode");
                        // 物料筛选
                        if (materialCode.equals(stockMaterialCode)) {
                            if (StringUtils.isNoneBlank(batchNo)) {
                                // 指定批次代表不能混批
                                boolean sameBatch = true;
                                for (Document s : v) {
                                    if (!Objects.equals(batchNo, s.get("batchNo"))) {
                                        sameBatch = false;
                                        break;
                                    }
                                }
                                if (sameBatch) {
                                    double use = v.stream().mapToDouble(xx -> ConvertUtils.convertIfPossible(xx.get("realNum"), Double.class, 0D)).sum();
                                    if (capacity > use) {
                                        priority = Math.max(priority, use);
                                        Document doc = new Document(x);
                                        doc.put("use", use);
                                        doc.put("partitionSide", partitionSide);
                                        doc.put("stocks", v);
                                        list.add(doc);
                                    }
                                } else {
                                    // 混批情况淘汰
                                }
                            } else {
                                // 未指定批次，可以混批
                                double use = v.stream().mapToDouble(xx -> ConvertUtils.convertIfPossible(xx.get("realNum"), Double.class, 0D)).sum();
                                if (capacity > use) {
                                    priority = Math.max(priority, use);
                                    Document doc = new Document(x);
                                    doc.put("use", use);
                                    doc.put("partitionSide", partitionSide);
                                    doc.put("stocks", v);
                                    list.add(doc);
                                }
                            }
                        }
                    } else {
                        Document doc = new Document(x);
                        doc.put("use", zero);
                        doc.put("partitionSide", partitionSide);
                        doc.put("stocks", new LinkedList<>());
                        list.add(doc);
                    }
                }
                for (Document document : list) {
                    document.put("priority", priority);
                }
                result.addAll(list);
            }
        });
        Collections.sort(result, (o1, o2) -> (int) (safeGetDouble(o2, "priority") - safeGetDouble(o1, "priority")));
        return result;
    }

    @Override
    public List<Document> queryMatchMixedMaterialContainers(List<String> areas, List<String> materialCodes, String batchNo,
                                                            Map<String, Double> capacityMapByMaterialCode,
                                                            Collection<String> excludeContainerCodes) throws BusinessException {
        // 根据容器类型筛选出容器编码
        Criteria criteria = newCriteria().and("type").is(newDocument("id", new ObjectId(mixedMaterial.getCode())))
                .and("lock").is(false);
        if(!CollectionUtils.isEmpty(excludeContainerCodes)) {
            criteria.and("code").nin(excludeContainerCodes);
        }
        Query query = Query.query(criteria);
        query.fields().include("code");
        List<Document> containers = mongoTemplate.find(query, Document.class, "entity_" + CONTAINER);
        assertBiz(() -> CollectionUtils.isEmpty(containers), "没有类型[%s]空余的容器或都被锁定", mixedMaterial.getCode());
        List<String> containerCodes = containers.stream().map(x -> x.getString("code")).collect(Collectors.toList());

        // 根据库区进行再次筛选
        Criteria match = newCriteria().and("area").in(areas)
                .and("lock").is(false).and("containerCode").in(containerCodes);
        List<AggregationOperation> bindOperations = new LinkedList<>();
        bindOperations.add(Aggregation.match(match));

        String sql = "{ $lookup:{ " +
                "from:'entity_6690d464634ce45c56c08921'," +
                "let:{ containerCode:'$containerCode', locationCode:'$code'}," +
                "pipeline:[" +
                "{$match:{$and:[{$expr:{$eq:['$containerCode','$$containerCode']}},{$expr:{$eq:['$isDelete',0]}}, " +
                "{$expr:{$gt:['$realNum', 0]}}, {$expr:{$eq:['$locationCode','$$locationCode']}}]}}" +
                "{$project:{materialCode:1, partitionSide:1, batchNo: 1, realNum: 1, _id:0}}" +
                "]," +
                "as:'stocks'}}";
        bindOperations.add(new CustomAggregationOperation(sql));
        bindOperations.add(Aggregation.lookup("entity_" + STOCK_AREA, "areaCode", "code", "areaObj"));
        bindOperations.add(Aggregation.unwind("areaObj", true));
        bindOperations.add(Aggregation.addFields()
                .addFieldWithValue("warehouseCode", "$areaObj.warehouse")
                .addFieldWithValue("areaCode", "$area")
                .addFieldWithValue("locationCode", "$code")
                .build());
        bindOperations.add(Aggregation.project().andInclude("warehouseCode", "areaCode", "locationCode", "containerCode", "stocks"));
        List<Document> locationStocks = aggregateResult(bindOperations, "entity_" + STOCK_LOCATION);

        double zero = 0D;
        // 整理数据
        // 隔板容器：根据隔板划分的空间来将容器划分开
        // 算出已使用空间
        // 按照已使用空间从大到校排列
        List<Document> result = new LinkedList<>();
        locationStocks.forEach(x -> {
            List<Document> stocks = x.getList("stocks", Document.class);
            if (stocks.isEmpty()) {
                materialCodes.forEach(m -> {
                    Document doc = new Document(x);
                    doc.put("use", zero);
                    doc.put("priority", zero);
                    doc.put("materialCode", m);
                    doc.put("partitionSide", "1");
                    result.add(doc);
                });
            } else {
                double priority = 0;
                List<Document> list = new LinkedList<>();
                Map<String, List<Document>> stockListMapByMaterialCode = new LinkedHashMap<>();
                stocks.forEach(s -> stockListMapByMaterialCode.computeIfAbsent(s.getString("materialCode"), a -> new LinkedList<>()).add(s));
                if(materialCodes.containsAll(stockListMapByMaterialCode.keySet())) {
                    // 传入批次不为空表示批次不能混
                    boolean pass = true;
                    if(StringUtils.isNotBlank(batchNo)) {
                        out: for (List<Document> ll : stockListMapByMaterialCode.values()) {
                            for (Document stock : ll) {
                                if(!Objects.equals(stock.getString("batchNo"), batchNo)) {
                                    pass = false;
                                    break out;
                                }
                            }
                        }
                    }
                    if(pass) {
                        // 整理每个物料存放量
                        for (String m : materialCodes) {
                            List<Document> v = stockListMapByMaterialCode.get(m);
                            double use = 0D;
                            if(!CollectionUtils.isEmpty(v)) {
                                use = v.stream().mapToDouble(xx -> ConvertUtils.convertIfPossible(xx.get("realNum"), Double.class, 0D)).sum();
                            }
                            Double capacity = capacityMapByMaterialCode.get(m);
                            if(capacity == 0) {
                                capacity = 0D;
                            }
                            if(capacity > use) {
                                Document doc = new Document(x);
                                doc.put("use", use);
                                priority = Math.max(priority, use);
                                doc.put("materialCode", m);
                                doc.put("stocks", v);
                                doc.put("partitionSide", "1");
                                list.add(doc);
                            }
                        }
                    }
                } else {
                    // 实际存储物料与混物料规则不一致，不匹配
                }
                for (Document document : list) {
                    document.put("priority", priority);
                }
                result.addAll(list);
            }
        });
//        Collections.sort(result, (o1, o2) -> (int) (o2.getDouble("priority") - o1.getDouble("priority")));
        Collections.sort(result, (o1, o2) -> (int) (safeGetDouble(o2, "priority") - safeGetDouble(o1, "priority")));
        return result;
    }

    /**
     * 查询空容器
     */
    @Override
    public List<Document> queryEmptyContainers(String containerType, List<String> areas) throws BusinessException {
        // 根据容器类型筛选出容器编码
        Query query = Query.query(newCriteria().and("type").is(newDocument("id", new ObjectId(containerType)))
                .and("lock").is(false));
        query.fields().include("code");
        List<Document> containers = mongoTemplate.find(query, Document.class, "entity_" + CONTAINER);
        if (CollectionUtils.isEmpty(containers)) {
            throw new BusinessException("没有改类型空余的容器或都被锁定");
        }
        List<String> containerCodes = containers.stream().map(x -> x.getString("code")).collect(Collectors.toList());

        // 根据库区进行再次筛选
        Criteria match = newCriteria().and("area").in(areas)
                .and("lock").is(false).and("containerCode").in(containerCodes);
        List<AggregationOperation> bindOperations = new LinkedList<>();
        bindOperations.add(Aggregation.match(match));

        String sql = "{ $lookup:{ " +
                "from:'entity_6690d464634ce45c56c08921'," +
                "let:{ containerCode:'$containerCode', locationCode:'$code'}," +
                "pipeline:[" +
                "{$match:{$and:[{$expr:{$eq:['$containerCode','$$containerCode']}},{$expr:{$eq:['$isDelete',0]}}, " +
                "{$expr:{$gt:['$realNum', 0]}}, {$expr:{$eq:['$locationCode','$$locationCode']}}]}}" +
                "{$project:{materialCode:1, partitionSide:1, batchNo: 1, realNum: 1, _id:0}}" +
                "]," +
                "as:'stocks'}}";
        bindOperations.add(new CustomAggregationOperation(sql));
        bindOperations.add(Aggregation.lookup("entity_" + STOCK_AREA, "areaCode", "code", "areaObj"));
        bindOperations.add(Aggregation.unwind("areaObj", true));
        bindOperations.add(Aggregation.addFields()
                .addFieldWithValue("warehouseCode", "$areaObj.warehouse")
                .addFieldWithValue("areaCode", "$area")
                .addFieldWithValue("locationCode", "$code")
                .build());
        bindOperations.add(Aggregation.project().andInclude("warehouseCode", "areaCode", "locationCode", "containerCode", "stocks"));
        List<Document> locationStocks = aggregateResult(bindOperations, "entity_" + STOCK_LOCATION);

        // 只需要无库存信息的容器
        List<Document> result = new LinkedList<>();
        locationStocks.forEach(x -> {
            List<Document> stocks = x.getList("stocks", Document.class);
            if (stocks.isEmpty()) {
                result.add(new Document(x));
            }
        });
        return result;
    }

    /**
     * 查询符合条件的容器
     *
     * @param containerType
     * @param areas
     * @param materialCode
     * @param colorName
     * @param languageName
     * @param batchNo
     * @param capacity
     */
    @Override
    public List<Document> queryMatchContainers(String containerType, List<String> areas, String materialCode,
                                               String colorName, String languageName, String batchNo, double capacity) throws BusinessException {
        // 根据容器类型筛选出容器编码
        Query query = Query.query(newCriteria().and("type").is(newDocument("id", new ObjectId(containerType)))
                .and("lock").is(false));
        query.fields().include("code");
        List<Document> containers = mongoTemplate.find(query, Document.class, "entity_" + CONTAINER);
        if (CollectionUtils.isEmpty(containers)) {
            throw new BusinessException("没有改类型空余的容器或都被锁定");
        }
        List<String> containerCodes = containers.stream().map(x -> x.getString("code")).collect(Collectors.toList());

        // 根据库区进行再次筛选
        Criteria match = newCriteria().and("area").in(areas)
                .and("lock").is(false).and("containerCode").in(containerCodes);
        List<AggregationOperation> bindOperations = new LinkedList<>();
        bindOperations.add(Aggregation.match(match));

        String sql = "{ $lookup:{ " +
                "from:'entity_6690d464634ce45c56c08921'," +
                "let:{ containerCode:'$containerCode', locationCode:'$code'}," +
                "pipeline:[" +
                "{$match:{$and:[{$expr:{$eq:['$containerCode','$$containerCode']}},{$expr:{$eq:['$isDelete',0]}}, " +
                "{$expr:{$gt:['$realNum', 0]}}, {$expr:{$eq:['$locationCode','$$locationCode']}}]}}" +
                "{$project:{materialCode:1, partitionSide:1, batchNo:1, realNum:1, colorName:1, languageName:1, _id:0}}" +
                "]," +
                "as:'stocks'}}";
        bindOperations.add(new CustomAggregationOperation(sql));
        bindOperations.add(Aggregation.lookup("entity_" + STOCK_AREA, "areaCode", "code", "areaObj"));
        bindOperations.add(Aggregation.unwind("areaObj", true));
        bindOperations.add(Aggregation.addFields()
                .addFieldWithValue("warehouseCode", "$areaObj.warehouse")
                .addFieldWithValue("areaCode", "$area")
                .addFieldWithValue("locationCode", "$code")
                .build());
        bindOperations.add(Aggregation.project().andInclude("warehouseCode", "areaCode", "locationCode", "containerCode", "stocks"));
        List<Document> locationStocks = aggregateResult(bindOperations, "entity_" + STOCK_LOCATION);

        // 根据类型来指定分隔数量，默认是普通的料箱
        int partitionSize;
        if (partitionBox.getCode().equals(containerType)) {
            partitionSize = 2;
        } else if (partitionBox3.getCode().equals(containerType)) {
            partitionSize = 3;
        } else if (partitionBox4.getCode().equals(containerType)) {
            partitionSize = 4;
        } else if (bigRack4.getCode().equals(containerType)) {
            partitionSize = 4;
        } else if (lightweight6.getCode().equals(containerType)) {
            partitionSize = 6;
        } else if (productBox2.getCode().equals(containerType)) {
            partitionSize = 2;
        } else {
            partitionSize = 1;
        }

        double zero = 0D;

        // 整理数据
        // 隔板容器：根据隔板划分的空间来将容器划分开
        // 算出已使用空间
        // 按照已使用空间从大到校排列
        List<Document> result = new LinkedList<>();
        locationStocks.forEach(x -> {
            List<Document> stocks = x.getList("stocks", Document.class);
            if (stocks.isEmpty()) {
                for (int i = 0; i < partitionSize; i++) {
                    Document doc = new Document(x);
                    doc.put("use", zero);
                    doc.put("priority", zero);
                    doc.put("partitionSide", i + 1 + "");
                    result.add(doc);
                }
            } else {
                Map<String, List<Document>> stockListMap = new LinkedHashMap<>();
                stocks.forEach(s -> stockListMap.computeIfAbsent(s.getString("partitionSide"), a -> new LinkedList<>()).add(s));
                double priority = 0;
                List<Document> list = new LinkedList<>();
                for (int i = 0; i < partitionSize; i++) {
                    String partitionSide = i + 1 + "";
                    List<Document> v = stockListMap.get(partitionSide);

                    if (v != null) {
                        String stockMaterialCode = v.get(0).getString("materialCode");
                        // 物料筛选
                        if (materialCode.equals(stockMaterialCode)) {
                            if (StringUtils.isNoneBlank(batchNo)) {
                                // 指定批次代表不能混批
                                boolean sameBatch = true;
                                for (Document s : v) {
                                    if (!s.getString("batchNo").equals(batchNo)) {
                                        sameBatch = false;
                                        break;
                                    }
                                }
                                if (sameBatch) {
                                    double use = v.stream().mapToDouble(xx -> ConvertUtils.convertIfPossible(xx.get("realNum"), Double.class, 0D)).sum();
                                    if (capacity > use) {
                                        priority = Math.max(priority, use);
                                        Document doc = new Document(x);
                                        doc.put("use", use);
                                        doc.put("partitionSide", partitionSide);
                                        doc.put("stocks", v);
                                        list.add(doc);
                                    }
                                } else {
                                    // 混批情况淘汰
                                }
                            } else {
                                // 未指定批次，可以混批
                                double use = v.stream().mapToDouble(xx -> ConvertUtils.convertIfPossible(xx.get("realNum"), Double.class, 0D)).sum();
                                if (capacity > use) {
                                    priority = Math.max(priority, use);
                                    Document doc = new Document(x);
                                    doc.put("use", use);
                                    doc.put("partitionSide", partitionSide);
                                    doc.put("stocks", v);
                                    list.add(doc);
                                }
                            }
                        }
                    } else {
                        Document doc = new Document(x);
                        doc.put("use", zero);
                        doc.put("partitionSide", partitionSide);
                        doc.put("stocks", new LinkedList<>());
                        list.add(doc);
                    }
                }
                for (Document document : list) {
                    document.put("priority", priority);
                }
                result.addAll(list);
            }
        });
//        Collections.sort(result, (o1, o2) -> (int) (o2.getDouble("priority") - o1.getDouble("priority")));
        Collections.sort(result, (o1, o2) -> (int) (safeGetDouble(o2, "priority") - safeGetDouble(o1, "priority")));
        return result;
    }

    @Override
    public List<Document> queryStocks(String materialCode, List<String> areas, String locationCode, String batchNo) {
        Criteria criteria = newCriteria().and("materialCode").is(materialCode).and("areaCode").in(areas);
        if(StringUtils.isNoneBlank(locationCode)) {
            criteria = criteria.and("locationCode").is(locationCode);
        }
        if(StringUtils.isNoneBlank(batchNo)) {
            criteria = criteria.and("batchNo").is(batchNo);
        }
        List<Document> stocks = mongoTemplate.find(Query.query(criteria), Document.class, "entity_" + STOCK_NAME);
        // 过滤掉被锁定的资源
        Set<String> locationCodes = new LinkedHashSet<>();
        Set<String> containerCodes = new LinkedHashSet<>();
        stocks.forEach(x -> {
            locationCodes.add(x.getString("locationCode"));
            containerCodes.add(x.getString("containerCode"));
        });

        List<Document> freeLocations = mongoTemplate.find(Query.query(newCriteria().and("code").in(locationCodes).and("lock").is(false)),
                Document.class, "entity_" + STOCK_LOCATION);
        if(CollectionUtils.isEmpty(freeLocations)) {
            log.info("物料[{}]库位[{}]批次[{}]的库存对应库位被锁定", materialCode, locationCodes, batchNo);
            return Collections.emptyList();
        }
        Set<String> freeLocationCodes = freeLocations.stream().map(x -> x.getString("code")).collect(Collectors.toSet());

        List<Document> freeContainers = mongoTemplate.find(Query.query(newCriteria().and("code").in(containerCodes).and("lock").is(false)),
                Document.class, "entity_" + CONTAINER);
        if(CollectionUtils.isEmpty(freeLocations)) {
            log.info("物料[{}]库位[{}]批次[{}]的库存对应容器被锁定", materialCode, locationCodes, batchNo);
            return Collections.emptyList();
        }
        Set<String> freeContainerCodes = freeContainers.stream().map(x -> x.getString("code")).collect(Collectors.toSet());

        return stocks.stream().filter(x -> freeLocationCodes.contains(x.getString("locationCode"))
                && freeContainerCodes.contains(x.getString("containerCode"))).collect(Collectors.toList());
    }

    @Override
    public List<Document> statsLocationContainer() {
        // [{"key":"realNum","symbol":"gt","value":0},{"key":"locationCode","symbol":"not"},{"key":"areaCode","symbol":"in","value":["100104","100203"]}]
        Criteria match = newCriteria().and("area").in(new String[]{"100104","100203"});
        List<AggregationOperation> bindOperations = new LinkedList<>();
        bindOperations.add(Aggregation.match(match));
        String sql = "{ $lookup:{ " +
                "from:'entity_6690d464634ce45c56c08921'," +
                "let:{ containerCode:'$containerCode', locationCode:'$code'}," +
                "pipeline:[" +
                "{$match:{$and:[{$expr:{$eq:['$containerCode','$$containerCode']}},{$expr:{$eq:['$isDelete',0]}}, " +
                "{$expr:{$gt:['$realNum', 0]}}, {$expr:{$eq:['$locationCode','$$locationCode']}}]}}" +
                "{$project:{materialCode:1, partitionSide:1, batchNo:1, realNum:1, colorName:1, languageName:1, _id:0}}" +
                "]," +
                "as:'stocks'}}";
        bindOperations.add(new CustomAggregationOperation(sql));
        bindOperations.add(Aggregation.project("code", "area", "containerCode", "stocks"));
        bindOperations.add(Aggregation.addFields()
                .addFieldWithValue("是否装货", ConditionalOperators.when(
                        ComparisonOperators.Gt.valueOf(
                                ArrayOperators.arrayOf("stocks").length()).greaterThanValue(0))
                        .then("是").otherwise("否"))
                        .addFieldWithValue("库位", "$code")
                        .addFieldWithValue("区域", "$area")
                        .addFieldWithValue("箱号", "$containerCode")
                        .addFieldWithValue("库存", "$stocks")
                .build());
        bindOperations.add(Aggregation.project().andExclude("code", "area", "containerCode", "stocks"));
        Aggregation aggregation = Aggregation.newAggregation(bindOperations);
        System.out.println(aggregation);
        return mongoTemplate.aggregate(aggregation, "entity_" + STOCK_LOCATION, Document.class).getMappedResults();
    }
}
